#include "kcache.h"

#include <sysapi.h>
#include <khashmap.h>
#include "kstlbool.h"

#include <memory.h>
#include <time.h>
#include <assert.h>

typedef struct KstlCache {
    KstlCacheSettings settings;
    KstlHashMap*      map;
    KstlList*         list;
    KstlList*         recycling;
} KstlCache;

typedef struct KSTLCacheListData {
    unsigned long long insertionTime;
    void*              key;
    size_t             keyLength;
} CacheListData;

static void UserDealloc(KstlCache* cache, KstlListNode* node)
{
    if (cache->settings.dealloc.deallocate) {
        void* context = cache->settings.dealloc.deallocatorContext;
        void* pItem   = (char*)KstlListPayload(node) + sizeof(CacheListData);
        cache->settings.dealloc.deallocate(context, pItem);
    }
}

static void RecycleNode(KstlCache* cache, KstlListNode* node)
{
    UserDealloc(cache, node);

    KstlListExtractNode(cache->list, node);
    KstlListPushNodeFront(cache->recycling, node);
}

static int ValidateExpired(KstlCache* cache, KstlListNode* node)
{
    if (!node) {
        return 0;
    }

    CacheListData*     data        = KstlListPayload(node);
    unsigned long long currentTime = KstlSteadyTimeNano();

    bool isExpired = currentTime - data->insertionTime > cache->settings.ttl;
    if (!isExpired) {
        return 0;
    }

    KstlHashMapErase(cache->map, data->key, data->keyLength);
    RecycleNode(cache, node);

    return 1;
}

static void ValidateAmount(KstlCache* cache)
{
    if (KstlCacheSize(cache) < cache->settings.maxElements) {
        return;
    }

    CacheListData* data = KstlListPayload(cache->list->head);
    KstlHashMapErase(cache->map, data->key, data->keyLength);
    RecycleNode(cache, cache->list->head);
}

static void* AddNewNode(KstlCache* cache)
{
    return KstlListPushRawBack(cache->list);
}

static void* ReuseOldNode(KstlCache* cache)
{
    KstlListNode* node = KstlListPopNodeFront(cache->recycling);
    KstlListPushNodeBack(cache->list, node);

    return KstlListPayload(node);
}

KstlCache* KstlCacheInit(KstlCacheSettings settings)
{
    if (settings.itemSize == 0 || settings.maxElements == 0) {
        return NULL;
    }

    KstlCache* cache = KstlAllocate(sizeof(KstlCache));
    if (!cache) {
        return NULL;
    }

    memset(cache, 0, sizeof(KstlCache));

    KstlHashMapSettings mapSettings = {.itemSize = sizeof(KstlListNode*)};
    cache->map                      = KstlHashMapInit(mapSettings);
    if (!cache->map) {
        KstlCacheRelease(cache);
        return NULL;
    }

    KstlListSettings listSettings = {.itemSize = sizeof(CacheListData) + settings.itemSize};

    cache->list = KstlListInit(listSettings);
    if (!cache->list) {
        KstlCacheRelease(cache);
        return NULL;
    }

    cache->recycling = KstlListInit(listSettings);
    if (!cache->recycling) {
        KstlCacheRelease(cache);
        return NULL;
    }

    cache->settings = settings;

    return cache;
}

void KstlCacheRelease(KstlCache* cache)
{
    if (cache->map) {
        KstlHashMapRelease(cache->map);
    }

    if (cache->list) {
        if (cache->settings.dealloc.deallocate) {
            KL_FOR_EACH(cache->list, it, end)
            {
                cache->settings.dealloc.deallocate(
                    cache->settings.dealloc.deallocatorContext,
                    (char*)KstlListIteratorValue(it) + sizeof(CacheListData)
                );
            }
        }
        KstlListRelease(cache->list);
    }

    if (cache->recycling) {
        KstlListRelease(cache->recycling);
    }

    if (cache) {
        KstlRelease(cache);
    }
}

size_t KstlCacheSize(KstlCache* cache)
{
    return KstlHashMapSize(cache->map);
}

int KstlCacheAdd(KstlCache* cache, const void* key, size_t keyLength, void* item)
{
    ValidateAmount(cache);
    ValidateExpired(cache, cache->list->head);

    CacheListData*      payload;
    KstlHashMapIterator it = KstlHashMapFind(cache->map, key, keyLength);
    if (KstlHashMapIteratorEqual(it, KstlHashMapIteratorEnd(cache->map))) {
        payload = KstlListEmpty(cache->recycling) ? AddNewNode(cache) : ReuseOldNode(cache);
        it      = KstlHashMapAdd(cache->map, key, keyLength, &cache->list->tail);
    } else {
        KstlListNode* node = *(KstlListNode**)KstlHashMapIteratorValue(it);
        UserDealloc(cache, node);
        payload = KstlListPayload(node);
    }

    payload->insertionTime = KstlSteadyTimeNano();
    payload->key           = KstlHashMapIteratorKey(it);
    payload->keyLength     = KstlHashMapIteratorKeySize(it);

    memcpy(payload + 1, item, cache->settings.itemSize);
    return 0;
}

int KstlCacheGet(KstlCache* cache, const void* key, size_t keyLength, void* item)
{
    ValidateExpired(cache, cache->list->head);

    KstlListNode* node   = NULL;
    int           result = KstlHashMapGet(cache->map, key, keyLength, &node);
    if (result) {
        return -1;
    }

    result = ValidateExpired(cache, node);
    if (result) {
        return -1;
    }

    CacheListData* data    = KstlListPayload(node);
    void*          payload = data + 1;

    if (item) {
        memcpy(item, payload, cache->settings.itemSize);
    }

    return 0;
}

int KstlCacheRemove(KstlCache* cache, const void* key, size_t keyLength)
{
    KstlHashMapIterator it = KstlHashMapFind(cache->map, key, keyLength);
    if (KstlHashMapIteratorEqual(it, KstlHashMapIteratorEnd(cache->map))) {
        return -1;
    }

    KstlListNode* node = *(KstlListNode**)KstlHashMapIteratorValue(it);
    RecycleNode(cache, node);

    KstlHashMapDelete(cache->map, it);

    return 0;
}

void KstlCacheRemoveIt(KstlCache* cache, KstlCacheIterator it)
{
    KstlHashMapErase(cache->map, KstlCacheIteratorKey(it), KstlCacheIteratorKeySize(it));
    RecycleNode(cache, it.current.current);
}

void KstlCacheClear(KstlCache* cache)
{
    KstlHashMapClear(cache->map);

    while (!KstlListEmpty(cache->list)) {
        RecycleNode(cache, cache->list->head);
    }
}

KstlCacheIterator KstlCacheIteratorBegin(KstlCache* handle)
{
    KstlCacheIterator it = {.handle = handle, .current = KstlListIteratorBegin(handle->list)};
    return it;
}


KstlCacheIterator KstlCacheIteratorEnd(KstlCache* handle)
{
    KstlCacheIterator it = {.handle = handle, .current = KstlListIteratorEnd(handle->list)};
    return it;
}

void KstlCacheIteratorNext(KstlCacheIterator* it)
{
    KstlListIteratorNext(&it->current);
}

bool KstlCacheIteratorIsEnd(KstlCacheIterator it)
{
    return KstlListIteratorEqual(it.current, KstlListIteratorEnd(it.handle->list));
}

void* KstlCacheIteratorValue(KstlCacheIterator it)
{
    return (CacheListData*)KstlListIteratorValue(it.current) + 1;
}

void* KstlCacheIteratorKey(KstlCacheIterator it)
{
    CacheListData* data = KstlListIteratorValue(it.current);
    return data->key;
}

size_t KstlCacheIteratorKeySize(KstlCacheIterator it)
{
    CacheListData* data = KstlListIteratorValue(it.current);
    return data->keyLength;
}
